/*
 *  Copyright 2010 UPF Telecos 4th group G.
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *  under the License.
 */
package edu.upf.core;

import edu.upf.messages.GenericMessage;
import edu.upf.messages.UserContent;
import edu.upf.utils.LogSystem;
import edu.upf.network.impl.XMPPClient;
import edu.upf.content.DescriptionContent;
import edu.upf.content.ImageObject;
import edu.upf.content.Video;
import edu.upf.database.impl.MediaDAO;
import edu.upf.exceptions.NoServerConnection;
import edu.upf.messages.SearchResult;
import edu.upf.messages.SearchProfile;
import edu.upf.network.ContentTransfer;
import edu.upf.userinterface.UserChat;
import edu.upf.userinterface.UserInterface;
import edu.upf.userinterface.impl.GraphicChat;
import edu.upf.userinterface.impl.GraphicInterface;
import edu.upf.userinterface.impl.SplashScreen;
import edu.upf.userinterface.impl.TextChat;
import edu.upf.userinterface.impl.TextInterface;
import edu.upf.utils.Definitions;
import edu.upf.utils.FormatChecker;
import edu.upf.vod.VoDClient;
import edu.upf.vod.VoDServer;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import javax.swing.UIManager;
import org.jivesoftware.smack.packet.Message;

/**
 * Class that handles all the interactions between objects, the controller class
 * in MVC
 * @author netjois
 */
public class ClientLogic implements Definitions {

    private XMPPClient jabberClient;
    private UserInterface userInterface;
    private MediaDAO mediaDAO;
    private String username;
    private UserList userList;
    private List<DescriptionContent> contentList;
    private List<ContentTransfer> transferList;
    private Map<String, UserChat> currentChatWindows;
    private LogSystem log;
    private boolean guiMode;
    private String lastExceptionMessage;
    private VoDServer vserv;
    private FileWriter fstream;
    private String vlcPath;
    private ConfigDiscover discover;
    private Process existProcess;
    private Thread errorOutput;
    /**
     * Constructor of the ClientLogic, depending on the parameter initializes a
     * text or graphical user interface
     * @param activateGUI 
     */
    public ClientLogic(boolean activateGUI) {
        log = new LogSystem(ClientLogic.class);
        discover = ConfigDiscover.getInstance(); 
        guiMode = activateGUI;
        if (guiMode){
            loadDB();
            new SplashScreen(10000);
        }
        initDataVariables();
        initUserInterface();
    }

    private void initDataVariables(){
        lastExceptionMessage="";
        userList = new UserList();
        contentList = new ArrayList();
        transferList = new ArrayList();
        currentChatWindows = new HashMap();
        mediaDAO = MediaDAO.getInstance(this);
    }

    private void initUserInterface(){
        if (guiMode) {
            initGraphicInterface();
            GraphicInterface gui = (GraphicInterface)userInterface;
            if(discover.isDBInstalled()){
                gui.showExistConfiguration(discover.getExistRoute(),true);
            }else{
                log.addInfoLog("eXist is not correctly configured");
                gui.showExistConfiguration(discover.getExistRoute(),false);
            }
            if(discover.isVLCInstalled()){
                gui.showVLCConfiguration(discover.getVLCRoute(),true);
                initVideoServer();
            }else{
                log.addInfoLog("vlc is not configured");
                gui.showExistConfiguration(discover.getVLCRoute(),false);
            }
        }else {
            initTextInterface();
        }
        userInterface.showContents();
    }

    public void loadDB(){
        String osName = System.getProperty("os.name");
        String command;
        if(osName.contains("Windows")){
            command =discover.getExistExecRoute();
        }else{
            command ="bash "+discover.getExistExecRoute();
        }
        try {
            log.addDebugLog("Loading DB: "+command);
            existProcess= Runtime.getRuntime().exec(command);
            errorOutput = new Thread() {
                @Override
                public void run() {
                    try {
                        InputStream is = existProcess.getErrorStream();
                        byte[] buffer = new byte[1024];
                        for (int count = 0; (count = is.read(buffer)) >= 0;) {
                            System.out.write(buffer, 0, count);
                        }
                    } catch (IOException ex) {
                        log.addInfoLog("eXist Process killed: "+ex.getMessage());
                    }
                }
            };
            errorOutput.start();
        } catch (IOException ex) {
            log.addInfoLog("Unable to load Database, please configure it correctly on Config panel");
        }
    }

    public void stopDB(){
        try {
            errorOutput.interrupt();
            existProcess.destroy();
            Process exitDB = Runtime.getRuntime().exec(discover.getExistStopRoute());
            exitDB.waitFor();
        } catch (InterruptedException ex) {
            log.addInfoLog("Error while database shutdown");
        } catch (IOException ex) {
            log.addInfoLog("Error while database shutdown");
        }
    }

    public boolean initVideoServer() {
        SearchResult searchresult;
        String sent1, sent2 = null;
        try {
            vlcPath = new java.io.File(".").getCanonicalPath();
            List videoList;
            if (mediaDAO != null) {
                searchresult = mediaDAO.getSharedVideos();
                videoList = searchresult.getList();
                Iterator i = videoList.iterator();
                fstream = new FileWriter(vlcPath + "/videoserver.vlm");
                BufferedWriter out = new BufferedWriter(fstream);
                while (i.hasNext()) {
                    DescriptionContent dc = (DescriptionContent) i.next();
                    sent1 = "new " + dc.getName() + " vod enabled";
                    sent2 = "setup " + dc.getName() + " input " + dc.getPath();
                    out.write(sent1 + "\n");
                    out.write(sent2 + "\n\n");
                }
                out.close();
            }
            vserv = new VoDServer();
            vserv.start();
            return true;
        } catch (IOException ex) {
            showExceptionMessage(ex, WARNING);
            return false;
        }
    }


    public boolean closeVideoServer() {
        if(vserv!=null){
            vserv.close();
            File configFile = new File(vlcPath + "/videoserver.vlm");
            if(configFile.exists()){
                configFile.delete();
            }
            return true;
        }
        return false;
    }

    /**
     * Sets the system look and feel for the swing GUI
     */
    private void setSystemLookandFeel() {
        try {
            UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
            log.addDebugLog("System look and feel set");
            //UIManager.setLookAndFeel("net.sourceforge.napkinlaf.NapkinLookAndFeel");
        } catch (Exception e) {
            log.addInfoLog("Failed to set look and feel" + e.getMessage());
        }
    }

    /**
     * Inits the graphic java swing interface
     */
    private void initGraphicInterface() {
        setSystemLookandFeel();
        //Disable hardware accelertion to work properly under any OS
        System.setProperty("com.sun.media.jai.disableMediaLib", "true");
        userInterface = new GraphicInterface(this);
        java.awt.EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                ((GraphicInterface) userInterface).setVisible(true);
            }
        });
    }

    private void initTextInterface() {
        userInterface = new TextInterface();
    }

    /**
     * Logs the user onto the server with the specified resource
     * @param user
     * @param pass
     * @param resource
     * @return
     */
    public boolean login(String user, String pass, String resource) {
        username = user;
        if (checkAddressIntegrity()) {
            jabberClient = new XMPPClient(this);
            return jabberClient.login(username, pass, resource);
        }
        return false;
    }

    /**
     * Logouts the user from the server
     * @return
     */
    public boolean logout() {
        boolean status = jabberClient.logout();
        jabberClient = null;
        return status;
    }

    /**
     * Called to stop an existing login process
     */
    public void interruptLogin() {
        jabberClient.stopLoginTask();
    }

    /**
     * Adds the server extension to a username introduced without it and checks
     * if the given login address is correct.
     */
    private boolean checkAddressIntegrity() {
        if(username==null){
            return false;
        }if (username.matches("^[a-zA-Z0-9_.]{2,20}\\@gmail\\.com$")) {
            return true;
        }if (username.matches("^[a-zA-Z0-9_.]{2,20}$")) {
            username += SERVER_EXTENSION;
            return true;
        }
        return false;
    }

    private boolean isValidAddress(String address){
        if(address==null){
            return false;
        }if (address.matches("^[a-zA-Z0-9_.]{2,20}\\@gmail\\.com$")) {
            return true;
        }
        return false;
    }

    /**
     * Adds a buddy to the buddy list
     * @param user
     * @return 
     */
    public boolean addBuddyUser(User user) {
        if(user!=null && isValidAddress(user.getAddress())){
            boolean userAddedSuccessfully = userList.addUser(user);
            if(userAddedSuccessfully){
                userInterface.addUserToList(user);
                return true;
            }
        }
        return false;
    }

    public boolean removeBuddyUser(User user) {
        if(user!=null && isValidAddress(user.getAddress())){
            return userList.removeUser(user);
        }
        return false;
    }

    /**
     * Returns the name of the user logged in
     * @return
     */
    public String getUserName() {
        return username;
    }

    /**
     * Returns the contact which is in the parameter position on the roster
     * list. Is the text that a user introduces on the client for logging in.
     * @param index
     * @return
     */
    public User getContact(int index) {
        return userList.getUser(index);
    }

    public User getContact(String address) {
        return userList.getUser(address);
    }

    /**
     * Returns the mail of the contact which is in the parameter position on the
     * roster list
     * @param index
     * @return
     */
    public String getContactMail(int index) {
        return userList.getUser(index).getAddress();
    }

    /**
     * Returns the current showed name of the contact given by the position
     * index on the roster.
     * @param index
     * @return
     */
    public String getBuddyName(int index) {
        return userList.getUser(index).getName();
    }

    /**
     * Processes a received chat message
     * @param msg
     */
    public void processChatMessage(Message msg) {
        String buddy = jabberClient.filterAddressResource(msg.getFrom());
        openUserChat(buddy).addText(msg.getBody());
        log.addDebugLog("Received chat msg from: " + buddy + ", Text: " + msg.getBody());
    }

    /**
     * Processes and incoming search query or profile
     * @param dest
     * @param query
     */
    public void processSearchQuery(String dest, SearchProfile query) {
        log.addDebugLog("Search query received: " + query.getQueryType() + ", " + query.getQuery());

        SearchResult result = null;
        if (query.getQueryType().equals(BASIC)) {
            result = processBasicQuery(query.getSubtype(), query.getQuery());
        } else if (query.getQueryType().equals(NEWS)) {
            result = processNewsQuery(query.getSubtype(), query.getQuery());
        } else if (query.getQueryType().equals(CATEGORY)) {
            result = processCategoryQuery(query.getSubtype(), query.getQuery());
        }
        jabberClient.sendGenericMessage(new GenericMessage(username, dest, result));
    }

    private SearchResult processBasicQuery(String subtype, String query) {
        if (subtype.equals(ALL)) {
            return mediaDAO.basicAllSearch(new String[]{query});
        } else if (subtype.equals(IMAGE)) {
            return mediaDAO.basicImageSearch(new String[]{query});
        } else if (subtype.equals(VIDEO)) {
            return mediaDAO.basicVideoSearch(new String[]{query});
        }
        return null;
    }

    private SearchResult processNewsQuery(String subtype, String query) {
        if (subtype.equals(ARTICLE)) {
            return mediaDAO.articleSearch(new String[]{query});
        } else if (subtype.equals(TITLE)) {
            return mediaDAO.titleSearch(new String[]{query});
        } else if (subtype.equals(PARAGRAPH)) {
            return mediaDAO.paragraphSearch(new String[]{query});
        }
        return null;
    }

    private SearchResult processCategoryQuery(String subtype, String query) {

        if (subtype.equals(SIMPLE)) {
            return mediaDAO.categorySearch(new String[]{query});
        } else if (subtype.equals(PLUS)) {
            String parameters[] = query.split(SPLITTER);
            return mediaDAO.categorySearch(parameters);
        }
        return null;
    }

    /**
     * Processes the received results associated to a search
     * @param orig
     * @param result
     */
    public void processSearchResult(String orig, SearchResult result) {
        log.addDebugLog("Received search results");
        for (int i = 0; i < result.contentLength(); i++) {
            DescriptionContent newContent = (DescriptionContent) result.getContent(i);
            newContent.setOwner(jabberClient.filterAddressResource(orig));
            userInterface.addResult(newContent);
            contentList.add(newContent);
        }
    }

    /**
     * Sends a search profile to all available contacts. True if the send was
     * succesful. It does not verify that the receivers got the message.
     * @param type
     * @param subtype
     * @param query
     * @return
     */
    public boolean sendSearchQuery(String type, String subtype, String query) {

        if (connectionOK()) {
            log.addDebugLog("Sending SearchProfile: " + query);
            SearchProfile searchProfile = new SearchProfile(type, subtype, query);

            jabberClient.removeResultListeners();
            jabberClient.addSearchResultListener(query);

            //Clear previous results shown
            contentList.clear();
            userInterface.clearResults();

            User nextContact;
            for (Iterator<User> i = userList.getOnlineUsers().iterator(); i.hasNext();) {
                nextContact = i.next();
                log.addDebugLog("Sending search to:" + nextContact.getAddress());
                jabberClient.sendGenericMessage(
                        new GenericMessage(username, nextContact.getAddress(), searchProfile));
            }
            return true;
        }else{
            showExceptionMessage(new NoServerConnection("Not connected to server"), WARNING);
        }
        return false;
    }

    /**
     * Sends a chat message to the given destination
     * @param dest
     * @param text
     * @return
     */
    public boolean sendChatMessage(String dest, String text) {
        if (connectionOK()) {
            log.addDebugLog("Sending chat: " + text + ", to: " + dest);
            jabberClient.sendChatMessage(dest, text);
        }
        return true;
    }

    public void openVideoClient(String ip, String path, String format, String video, boolean local) throws IOException, InterruptedException{

        VoDClient vodclient = new VoDClient(ip, video);
        if (local){
            vodclient.setLocalparam(path, format);
        }
        vodclient.viewvideo(local);
    }

    /**
     * Creates a chat session with the given buddy
     * @param buddy
     * @return
     */
    public UserChat openUserChat(String buddy) {

        UserChat currentChat = currentChatWindows.get(buddy);
        if (currentChat == null) {
            log.addDebugLog("Creating chat with buddy " + buddy);
            if (guiMode) {
                currentChat = new GraphicChat(buddy, this);
                ((GraphicChat) currentChat).setVisible(true);
            } else {
                currentChat = new TextChat();
            }
        }
        currentChatWindows.put(buddy, currentChat);
        return currentChat;
    }

    /**
     * Close the chat session with the given buddy
     * @param buddy
     * @return
     */
    public boolean closeChat(String buddy) {
        currentChatWindows.remove(buddy);
        log.addDebugLog("Deleting chat with buddy " + buddy);
        return true;
    }

    /**
     * Changes the current status of a user to a available presence or not
     * @param user
     * @return 
     */
    public boolean updateUserPresence(User user) {
        if(userList.updateUser(user)){
            log.addDebugLog("Updating user " + user.getName() + " presence, present="
                + user.isAvailable());
            userInterface.updateUserList(user);
            return true;
        }
        return false;
    }

    /**
     * Shows a log event (i.e. new buddy available) on the user space log system
     * @param logEntry
     */
    public void eventLog(String logEntry) {
        log.addDebugLog("Adding to the uintf log: " + logEntry);
        userInterface.userLog(logEntry);
    }

    /**
     * Initiates the download of the selected content using the identification
     * number associated to the list of search result contents showed. When
     * the download finishes, a callback function is called.
     * @param content
     * @param viewMode
     * @return
     */
    public boolean sendUserContentRequest(DescriptionContent content, boolean viewMode){

        if(connectionOK()){
            String sourceID = content.getPath();
            String listenerID = "";
            if(viewMode){
                listenerID=TEMP_FOLDER;
            }else{
                listenerID=DOWNLOADS_FOLDER;
            }
            listenerID+=content.getName()+"."+content.getFormat();
            jabberClient.addTransferListener(listenerID,viewMode);
            log.addDebugLog("Sending content request: "+sourceID);
            UserContent userContent = new UserContent(sourceID);
            GenericMessage msg = new GenericMessage(username, content.getOwner(), userContent);
            jabberClient.sendGenericMessage(msg);
            return true;
        }
        return false;
    }

    /**
     * Proccesses a finished download cleaning the listener and notifying
     * to user interface when necesary
     * @param viewMode
     * @param downloadID
     */
    public void processFinishedDownload(boolean viewMode, String downloadID){
        jabberClient.removeTransferListener(downloadID);
        DescriptionContent content = createDescriptionContent(downloadID);
        if(viewMode){
            userInterface.showViewerImage(content,true);
        }else{
            addMedia(content, content.getSize());
        }
    }

    public DescriptionContent createDescriptionContent(String contentPath){
        
        File contentFile = new File(contentPath);
        String name, format, size, album;
        
        //Split and get Album
        String[] splitted=contentPath.split("/");
        album=splitted[splitted.length-2];
        
        //Split and get Name and Format
        splitted = splitted[splitted.length - 1].split("\\.");
        name = splitted[0];
        for (int i = 1; i < splitted.length - 1; i++) {
            name += "." + splitted[i];
        }
        format=splitted[splitted.length-1];

        //Get Size in KB
        size=contentFile.length()/1000+"KB";

        log.addInfoLog("Generating new DescrContent: "+ contentPath
                +"\nName: "+name+" format: "+format+" size: "+size+" album: "+album);
        DescriptionContent generatedContent=null;
        if(FormatChecker.isImage(format)){
            generatedContent = new ImageObject();
        }else if(FormatChecker.isVideo(format)){
            generatedContent = new Video();
        }
        generatedContent.setFormat(format);
        generatedContent.setName(name);
        generatedContent.setSize(size);
        generatedContent.setAlbum(album);
        return generatedContent;
    }
    
    /**
     * Processes a received User Content request from the given client
     * @param orig
     * @param payload
     */
    public void processUserContentRequest(String orig, UserContent payload) {
        log.addDebugLog("Sending media with source: " + payload.getSourceID()
                +"To: "+orig);
        File media = new File(payload.getSourceID());
        log.addDebugLog("File "+media.getName()+" exists: " + media.exists());
        jabberClient.sendMedia(media, orig, media.getName());
    }

    /**
     * Processes the receiving content
     * @param transfer
     */
    public void addContentTransfer(ContentTransfer transfer) {
        transferList.add(transfer);
        userInterface.addTransfer(transfer);
        log.addDebugLog("New transfer started: "+transfer.getContentName());
    }
    
    /**
     * Informs the user about a change in the state of the software
     * @param state
     */
    public void updateState(int state) {
        userInterface.updateState(state);
    }

    /**
     * Shows an exception message to the user
     * @param ex
     * @param level
     */
    public void showExceptionMessage(Throwable ex, String level) {
        String exceptionMessage = ex.getMessage();
        log.addInfoLog(exceptionMessage);
        if (lastExceptionMessage.equals(exceptionMessage)) {
            return;
        }
        if(level.equals(ERROR)){
            lastExceptionMessage = exceptionMessage;
        }else{
            lastExceptionMessage="";
        }
        
        StackTraceElement[] stackTrace = ex.getStackTrace();
        String trace = stackTrace[0].getClassName()+"\nMethod: "
                +stackTrace[0].getMethodName()+", line: "
                +stackTrace[0].getLineNumber();
        userInterface.showExceptionMessage(level, exceptionMessage, trace);
    }

    public List getAlbums(){
        if(dbConnectionOK()){
            return mediaDAO.getAlbums().getList();
        }
        return null;
    }

    public void addMedia(DescriptionContent dc,String size){
        if(dbConnectionOK()){
            log.addInfoLog("Adding new media to database: "+ dc.getPath());
            String[] queryParam = new String[]{dc.getName(),dc.getAlbum(),"",
                                            dc.getFormat(),size,"","","","",""};
            if (dc instanceof ImageObject){
                mediaDAO.addImage(queryParam);
            } else if (dc instanceof Video){
                mediaDAO.addVideo(queryParam);
            }
            if(guiMode){
                ((GraphicInterface)userInterface).updateMediaLibrary();
            }
        }
    }

    public void updateOntology(DescriptionContent dc, String category){
        if(dbConnectionOK()){
            if (dc instanceof ImageObject){
                mediaDAO.updateImageCategory(new String[]{dc.getName(),dc.getAlbum(),category});
            } else if (dc instanceof Video){
                mediaDAO.updateVideoCategory(new String[]{dc.getName(),dc.getAlbum(),category});
            }
        }
    }

    public void changeAlbum(String albumname, String newalbumname){
        if(dbConnectionOK()){
            mediaDAO.changeAlbum(new String[]{albumname,newalbumname});
        }
    }

    public void deleteAlbum(String albumname){
        if(dbConnectionOK()){
            mediaDAO.deleteAlbum(new String[]{albumname});
        }
    }

    public void updateAlbum(DescriptionContent dc, String album){
        if(dbConnectionOK()){
            if (dc instanceof ImageObject){
                mediaDAO.updateImageAlbum(new String[]{dc.getName(),dc.getAlbum(),album});
            }else if (dc instanceof Video){
                mediaDAO.updateVideoAlbum(new String[]{dc.getName(),dc.getAlbum(),album});
            }
        }
    }

    public void deleteMedia(DescriptionContent dc){
        if(dbConnectionOK()){
            if (dc instanceof ImageObject){
                mediaDAO.deleteImage(new String[]{dc.getName(),dc.getAlbum()});
            } else if (dc instanceof Video) {
                mediaDAO.deleteVideo(new String[]{dc.getName(),dc.getAlbum()});
            }
        }
    }

    public void addTag(DescriptionContent dc, String tag){
        if(dbConnectionOK()){
            if (dc instanceof ImageObject){
                mediaDAO.addImageTag(new String[]{dc.getName(),dc.getAlbum(),tag});
            } else if (dc instanceof Video){
                mediaDAO.addVideoTag(new String[]{dc.getName(),dc.getAlbum(),tag});
            }
        }
    }

    /**
     * Returns the shared content on the given directory
     * @param directory 
     * @return
     */
    public List getSharedContent(String directory) {
        if(dbConnectionOK()){
            if(directory.equals("")){
                return mediaDAO.getSharedContent().getList();
            }else{
                return mediaDAO.getPathContent(directory).getList();
            }
        }
        return null;   
    }

    private boolean connectionOK() {
        if (jabberClient == null) {
            return false;
        }
        return true;
    }

    private boolean dbConnectionOK(){
        if (mediaDAO == null) {
            return false;
        }
        return true;
    }
}
